package com.apobates.jforum2.troll.forum.controller;

import com.apobates.jforum2.troll.attention.core.decorater.ForumEntityIcoDecorder;
import com.apobates.jforum2.troll.forum.biz.BoardConfigService;
import com.apobates.jforum2.troll.forum.biz.BoardGroupService;
import com.apobates.jforum2.troll.forum.biz.BoardService;
import com.apobates.jforum2.troll.forum.biz.BoardStatsService;
import com.apobates.jforum2.troll.forum.entity.Board;
import com.apobates.jforum2.troll.forum.entity.BoardConfig;
import com.apobates.jforum2.troll.forum.entity.BoardGroup;
import com.apobates.jforum2.troll.forum.entity.BoardStats;
import com.apobates.jforum2.troll.forum.mapper.BoardConfigVoMapperEntity;
import com.apobates.jforum2.troll.forum.mapper.BoardGroupVoMapperEntity;
import com.apobates.jforum2.troll.forum.mapper.BoardStatsVoMapperEntity;
import com.apobates.jforum2.troll.forum.mapper.BoardVoMapperEntity;
import com.apobates.jforum2.troll.forum.stub.vo.BoardCaseGroupVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardCollectStatsVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardConfigVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardGroupVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardModelVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardStatsCollectVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardStatsVo;
import com.apobates.jforum2.troll.forum.stub.vo.BoardVo;
import com.apobates.jforum2.troll.regular.ForumActionEnum;
import com.apobates.jforum2.troll.regular.ImageIOMeta;
import com.apobates.jforum2.troll.utils.core.Commons;
import com.apobates.jforum2.troll.utils.core.ResponseResult;
import com.apobates.jforum2.troll.utils.core.api.ActionEventCulpritor;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 版块Api
 *
 * @author xiaofanku
 * @since 20201113
 */
@RestController
@RequestMapping("/board")
public class BoardController {
    @Autowired
    private BoardService boardService;
    @Autowired
    private BoardStatsService boardStatsService;
    @Autowired
    private BoardConfigService boardConfigService;
    @Autowired
    private BoardGroupService boardGroupService;
    @Value("${img.bucket.domain}")
    private String imageBucketDomain;
    @Value("${img.bucket.upload.direct}")
    private String imageBucketUploadDirect;
    private final static Logger logger = LoggerFactory.getLogger(BoardController.class);

    //0.1:从话题移动中分离出来的事务()
    @PutMapping(value="/{id}/stats/balance", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> balanceTopic(@RequestParam("target")long targetBoardId, @PathVariable("id")long boardId){
        Supplier<Optional<Boolean>> resultFun = ()->boardStatsService.balanceTopic(targetBoardId, boardId);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "更新版块的话题统计失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //0.2:从话题移动中分离出来的事务(平衡版块统计中的话题数量)
    @PutMapping(value="/{id}/stats/balance/posts", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> balanceTopicPosts(@RequestParam("target")long targetBoardId, @PathVariable("id")long boardId, @RequestParam("replies")long replies){
        Supplier<Optional<Boolean>> resultFun = ()->boardStatsService.balanceTopicPosts(targetBoardId, boardId, replies);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "更新版块的话题统计失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //0.3:从话题发布中分离出来的事务(更新版块统计的最近话题)@20211006
    @PutMapping(value="/{id}/stats/update", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> updateStatsRecentTopic(
            @PathVariable("id")long boardId,
            @RequestParam("topic")long recentTopicId,
            @RequestParam("title")String recentTopicTitle,
            @RequestParam("member")long recentTopicMemberId,
            @RequestParam("nickname")String recentTopicMemberNickname){
        Supplier<Optional<Boolean>> resultFun = ()->boardStatsService.updateRecentTopic(boardId, recentTopicId, recentTopicTitle, recentTopicMemberId, recentTopicMemberNickname);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "更新版块统计的最近话题失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //0.4:从回复分离出来的事务(更新版块的回复数量)@20211006
    @PutMapping(value="/{id}/stats/update/replies", consumes="application/x-www-form-urlencoded")
    public ResponseEntity<String> updateStatsReplies(
            @PathVariable("id")long boardId,
            @RequestParam("topic")long recentTopicId,
            @RequestParam("posts")long recentPostsId){
        Supplier<Optional<Boolean>> resultFun = ()->boardStatsService.updatePostsSize(boardId, recentTopicId, recentPostsId);
        ResponseResult<String> rr = ResponseResult.ofBoolean(resultFun, "更新版块统计的回复数量失败");
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }

    //1:版块列表底部的统计
    @GetMapping("/collect")
    public ResponseEntity<BoardStatsCollectVo> getBoardCollectStats(@RequestParam(value="cycle", required=false, defaultValue="30") int statsCycle){
        Map<ForumActionEnum, Long> statsdata = boardStatsService.sumTopicAndPosts();
        BoardStatsCollectVo f = new BoardStatsCollectVo();
        f.setReplies(statsdata.getOrDefault(ForumActionEnum.POSTS_REPLY, 0L));
        f.setThreads(statsdata.getOrDefault(ForumActionEnum.TOPIC_PUBLISH, 0L));
        f.setStatsCycle(statsCycle+"");
        //
        ResponseResult<BoardStatsCollectVo> rr = ResponseResult.Helper.ofEntity(f);
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //2:指定的版块
    @GetMapping("/{id}")
    public ResponseEntity<BoardVo> getBoardById(@PathVariable("id")long boardId){
        Supplier<Optional<Board>> rs = ()->(boardId>0)?boardService.get(boardId):Optional.empty();
        ResponseResult<BoardVo> rr = ResponseResult.ofEntityVo(rs, new BoardVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    
    //3:指定版块的图标
    @GetMapping("/{id}/ico/{scale}")
    public ResponseEntity<String> getBoardIco(@PathVariable("id")long boardId, @PathVariable("scale")String scale){
        Optional<Board> board = (boardId>0)?boardService.get(boardId):Optional.empty();
        if(!board.isPresent()){
            return ResponseEntity.status(400).body("版块不存在");
        }
        String boardIcoURL = null;
        String defaultIco = String.format("%s/%s/ico/default_icon.png", getImageIOMeta().getImageBucketDomain(), getImageIOMeta().getUploadImageDirectName());
        try {
            boardIcoURL = new ForumEntityIcoDecorder(getImageIOMeta().getImageBucketDomain(), getImageIOMeta().getUploadImageDirectName())
                    .decode(board.get().getImageAddr(), scale)
                    .map(URL::toString)
                    .orElse(defaultIco);
        }catch (java.net.MalformedURLException e){
            boardIcoURL = defaultIco;
        }
        //
        ResponseResult<String> rr = ResponseResult.Helper.ofOptional(Optional.ofNullable(boardIcoURL));
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //4:指定版块组下的所有版块
    @GetMapping("/all/{volume}")
    public List<BoardVo> getAllBoardsByVolume(@PathVariable("volume")int boardGroupId){
        List<Board> rs = boardService.getAllUsedByVolumesId(boardGroupId);
        return rs.stream().map(BoardVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    
    //5:所有可用的版块
    @GetMapping("/all")
    public List<BoardVo> getAll(){
        return boardService.getAllUsed().map(BoardVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //6:指定的版块[并加载版块的统计]
    @GetMapping("/{id}/cascade/stats")
    public ResponseEntity<BoardCollectStatsVo> getBoardStatsById(@PathVariable("id")long boardId){
        CompletableFuture<Optional<BoardStats>> bs = CompletableFuture.supplyAsync(()->boardStatsService.getByBoard(boardId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        BoardCollectStatsVo rs = getBoardAsync(boardId).thenCombineAsync(bs, (boardOpt, boardStatsOpt)->{
            BoardVo bv = new BoardVoMapperEntity().transform(boardOpt).orElse(null);
            if(null == bv){
                return null;
            }
            BoardStatsVo bsv = new BoardStatsVoMapperEntity().transform(boardStatsOpt).orElse(new BoardStatsVo());
            BoardCollectStatsVo bcsv = new BoardCollectStatsVo();
            bcsv.setBoardVo(bv);bcsv.setStatsVo(bsv);
            return bcsv;
        }).orTimeout(1, TimeUnit.SECONDS).join();
        //
        ResponseResult<BoardCollectStatsVo> rr = ResponseResult.Helper.ofEntity(rs);
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    
    private CompletableFuture<Optional<Board>> getBoardAsync(long boardId){
        return CompletableFuture.supplyAsync(()->boardService.get(boardId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
    }
    
    //7:查看指定版块的统计
    @GetMapping("/{id}/stats")
    public ResponseEntity<BoardStatsVo> getBoardStats(@PathVariable("id")long boardId){
        //缺少今日的统计
        Supplier<Optional<BoardStats>> rs = ()->boardStatsService.getByBoard(boardId); //.getFillTodayByBoard(boardId);
        ResponseResult<BoardStatsVo> rr = ResponseResult.ofEntityVo(rs, new BoardStatsVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //8:查看多个版块的统计
    @PostMapping(value="/stats/multi", consumes="application/x-www-form-urlencoded")
    public List<BoardStatsVo> getMultiBoardStats(@RequestParam("records")String boardIdString){
        Set<Long> boardIdSet = Commons.toLongSet(boardIdString);
        if (null == boardIdSet || boardIdSet.isEmpty()) {
            return Collections.emptyList();
        }
        Stream<BoardStats> rs = boardStatsService.getFillTodayTopices(boardIdSet);
        return rs.map(BoardStatsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //9:所有的版块统计记录
    @GetMapping("/stats/all")
    public List<BoardStatsVo> getAllBoardStats(){
        Stream<BoardStats> rs = boardStatsService.getAll();
        return rs.map(BoardStatsVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    //10:查看指定版块并及联加载版块组
    @GetMapping("/{id}/volume/{volume}")
    public ResponseEntity<BoardCaseGroupVo> getBoardCascadeBoardGroup(@PathVariable("id")long boardId, @PathVariable("volume")int boardGroupId){
        CompletableFuture<Optional<BoardGroup>> bs = CompletableFuture.supplyAsync(()->boardGroupService.get(boardGroupId)).completeOnTimeout(Optional.empty(), 1, TimeUnit.SECONDS);
        BoardCaseGroupVo rs = getBoardAsync(boardId).thenCombineAsync(bs, (boardOpt, boardGroupOpt)->{
            BoardVo bv = new BoardVoMapperEntity().transform(boardOpt).orElse(null);
            if(null == bv){
                return null;
            }
            BoardGroupVo bgv = BoardGroupVoMapperEntity.mapper().apply(boardGroupOpt.orElse(BoardGroup.defaultInstance()));
            BoardCaseGroupVo bcgv = new BoardCaseGroupVo();
            bcgv.setBoardVo(bv);bcgv.setBoardGroupVo(bgv);
            return bcgv;
        }).orTimeout(1, TimeUnit.SECONDS).join();
        //
        ResponseResult<BoardCaseGroupVo> rr = ResponseResult.Helper.ofEntity(rs);
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    
    //11:指定的会员是否收藏过版块
    @GetMapping("/{id}/favorite/member/{member}")
    public boolean isFavoriteBoard(@PathVariable("id")long boardId, @PathVariable("member")long memberId){
        Optional<Boolean> actionFunction = Optional.ofNullable(boardService.isFavorited(boardId, memberId));
        return actionFunction.orElse(false);
    }
    //12:联合检查:
    // 1)是否可以收藏, 可以返回true
    /* Angular采用的
    @GetMapping("/{id}/check/{member}/operate")
    @Produces(MediaType.SERVER_SENT_EVENTS)
    public void getThreadsUnionResult(@Context SseEventSink eventSink, @Context Sse sse, @PathVariable("id") long boardId, @PathVariable("member") long memberId) {
        BoardUnionCheckVo rs = new BoardUnionCheckVo();
        rs.setId(boardId);
        rs.setFavorite(boardService.isFavorited(boardId, memberId)?"1":"0");
        //
        final OutboundSseEvent event = sse.newEventBuilder()
                .name("BoardUnionCheck")
                .mediaType(MediaType.APPLICATION_JSON_TYPE)
                .data(BoardUnionCheckVo.class, rs)
                .build();
        //CompletableFuture.runAsync(() -> eventSink.send(event)).whenComplete((r, ex) -> eventSink.close());
        eventSink.send(event);
    }*/
    
    //13:查看指定版块并加载版块统计
    @GetMapping("/multi/stats")
    public List<BoardCollectStatsVo> getMultiBoardAndStats(@RequestParam("boardIds") String boardIdString){
        Set<Long> args = Commons.toLongSet(boardIdString);
        CompletableFuture<Stream<Board>> rs = CompletableFuture.supplyAsync(()->boardService.getAllUsed(args)).completeOnTimeout(Stream.empty(), 1, TimeUnit.SECONDS);
        CompletableFuture<Map<Long,BoardStats>> bsMap = CompletableFuture.supplyAsync(()->boardStatsService.getByBoardIdList(args)).thenApply(bss->bss.collect(Collectors.toMap(BoardStats::getBoardId, Function.identity()))).completeOnTimeout(Collections.emptyMap(), 1, TimeUnit.SECONDS);
        //
        return rs.thenCombineAsync(bsMap, (boardStms, boardStatsMap)->{
            return boardStms.map(bs->{
                BoardVo csv = BoardVoMapperEntity.mapper().apply(bs);
                BoardStatsVo bsv = BoardStatsVoMapperEntity.mapper().apply(boardStatsMap.get(bs.getId()));
                //
                BoardCollectStatsVo bcsv = new BoardCollectStatsVo();
                bcsv.setBoardVo(csv);
                bcsv.setStatsVo(bsv);
                return bcsv;
            });
        }).completeOnTimeout(Stream.empty(), 1, TimeUnit.SECONDS).join().collect(Collectors.toList());
    }
    //14:查看指定版块并解码图标
    @GetMapping("/multi/icon")
    public List<BoardVo> getMultiBoardAndDecodeIco(@RequestParam("boardIds") String boardIdString,@RequestParam("scale")String scale){
        Stream<Board> rs = boardService.getAllUsed(Commons.toLongSet(boardIdString));
        String defaultIco = String.format("%s/%s/ico/default_icon.png", getImageIOMeta().getImageBucketDomain(), getImageIOMeta().getUploadImageDirectName());
        ForumEntityIcoDecorder fe = new ForumEntityIcoDecorder(getImageIOMeta().getImageBucketDomain(), getImageIOMeta().getUploadImageDirectName());
        return rs.map(b->{
            BoardVo bv = BoardVoMapperEntity.mapper().apply(b);
            try {
                String boardIcoURL = fe.decode(b.getImageAddr(), scale)
                        .map(URL::toString)
                        .orElse(defaultIco);
                bv.setImageAddr(boardIcoURL);
            }catch (java.net.MalformedURLException e){
                bv.setImageAddr(defaultIco);
            }
            return bv;
        }).collect(Collectors.toList());
    }

    //15:分页查看指定会员收藏的版块(改由Nest实现)
    //@GetMapping("/favorite/member/{member}/{page}/{number}")
    //public Response getMemberFavorites(@PathVariable("member") long memberId, @PathVariable("page") int page, @PathVariable("number") int pageSize){
    //    Page<BoardActionCollectionReplica> rs = boardService.getFavoritesForMember(memberId, new PageRequest(page, pageSize));
    //    return ResponseResult.PageResultBuilder.<BoardRecordVo, BoardActionCollectionReplica>getInstance(page, pageSize).from(rs.getTotalElements(), rs.getResult()).build(new BoardRecordVoMapperEntity()).toJson();
    //}
    //16:收藏
    @PutMapping(value="/{id}/favorite", consumes="application/x-www-form-urlencoded")
    public boolean favoriteBoard(
            @PathVariable("id")long boardId,
            @RequestParam("member")long memberId,
            @RequestParam("nickname") String memberNickname,
            @RequestParam("token") String token,
            @RequestParam("ip")String ipAddr){
        logger.error("[forum-api]CTL start favorite board");
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        try {
            Optional<Boolean> actionFunction = boardService.favorite(boardId, aec);
            return actionFunction.orElse(false);
        }catch (IllegalStateException e){
            logger.error("[forum-api]favorite like fail, exception:"+e.getMessage());
        }
        return false;
    }
    //17:取消收藏
    @PutMapping(value="/{id}/favorite/cancel", consumes="application/x-www-form-urlencoded")
    public boolean cancelFavorite(
            @PathVariable("id")long boardId,
            @RequestParam("member")long memberId,
            @RequestParam("nickname") String memberNickname,
            @RequestParam("token") String token,
            @RequestParam("ip")String ipAddr){
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        Optional<Boolean> actionFunction = boardService.removeFavorite(boardId, aec);
        return actionFunction.orElse(false);
    }
    //18:锁定
    @PutMapping(value="/{id}/lock", consumes="application/x-www-form-urlencoded")
    public boolean lockBoard(
            @PathVariable("id")long boardId,
            @RequestParam("volume")int boardGroupId,
            @RequestParam("member")long memberId,
            @RequestParam("nickname") String memberNickname,
            @RequestParam("token") String token,
            @RequestParam("ip")String ipAddr){
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        Optional<Boolean> actionFunction = boardService.lock(boardId, boardGroupId, aec);
        return actionFunction.orElse(false);
    }
    //19:解锁
    @PutMapping(value="/{id}/lock/cancel", consumes="application/x-www-form-urlencoded")
    public boolean cancelLock(
            @PathVariable("id")long boardId,
            @RequestParam("volume")int boardGroupId,
            @RequestParam("member")long memberId,
            @RequestParam("nickname") String memberNickname,
            @RequestParam("token") String token,
            @RequestParam("ip")String ipAddr){
        ActionEventCulpritor aec = getInstance(memberId, memberNickname, ipAddr, token);
        Optional<Boolean> actionFunction = boardService.releaseLock(boardId, boardGroupId, aec);
        return actionFunction.orElse(false);
    }
    //20:查看指定版块的配置文件
    @GetMapping("/{id}/config")
    public ResponseEntity<BoardConfigVo> getBoardConfig(@PathVariable("id")long boardId){
        Supplier<Optional<BoardConfig>> rs = ()->boardConfigService.getByBoardId(boardId);
        ResponseResult<BoardConfigVo> rr = ResponseResult.ofEntityVo(rs, new BoardConfigVoMapperEntity());
        return ResponseEntity.status(rr.getCode()).contentType(MediaType.APPLICATION_JSON).body(rr.getEntity());
    }
    //21:版块Top榜
    @GetMapping("/model")
    public List<BoardModelVo> getTopBoard() {
        Map<Long, String> boardMap = boardService.getAllUsed().collect(Collectors.toMap(Board::getId, Board::getTitle));
        Map<Long, BoardStatsVo> boardStatsMap = getAllBoardStats().stream().collect(Collectors.toMap(BoardStatsVo::getBoardId, Function.identity()));

        return boardMap.entrySet().stream().map(entry -> {
            BoardModelVo tmp = new BoardModelVo();
            tmp.setId(entry.getKey());
            tmp.setTitle(entry.getValue());
            BoardStatsVo t2 = boardStatsMap.getOrDefault(entry.getKey(), new BoardStatsVo());
            tmp.setReplies(t2.getReplies());
            tmp.setThreads(t2.getThreads());
            tmp.setLink(BoardVo.produceLink.apply(t2.getBoardId(), t2.getBoardGroupId()));
            return tmp;
        }).collect(Collectors.toList());
    }
    
    //22:查看指定的版块
    @GetMapping("/multi")
    public List<BoardVo> getMultiBoard(@RequestParam("boardIds") String boardIdString){
        return boardService.getAllUsed(Commons.toLongSet(boardIdString)).map(BoardVoMapperEntity.mapper()).collect(Collectors.toList());
    }
    
    private ImageIOMeta getImageIOMeta(){
        return new ImageIOMeta(){
            @Override
            public String getImageBucketDomain() {
                return imageBucketDomain;
            }

            @Override
            public String getUploadImageDirectName() {
                return imageBucketUploadDirect;
            }
        };
    }
    public static ActionEventCulpritor getInstance(final long memberId, final String memberNickname, final String ipAddr, final String token) {
        return new ActionEventCulpritor() {
            @Override
            public long getMemberId() {
                return memberId;
            }

            @Override
            public String getMemberNickname() {
                return memberNickname;
            }

            @Override
            public String getIpAddr() {
                return ipAddr;
            }

            @Override
            public String getToken() {
                return token;
            }

            @Override
            public String getUserAgent() {
                return "";
            }
        };
    }
}
/*
/board/collect
/board/{id}
/board/{id}/ico/{scale}
/board/{id}/stats
/board/{id}/cascade/stats
/board/{id}/volume/{volume}
/board/{id}/favorite/member/{member}
/board/{id}/favorite
/board/{id}/favorite/cancel
/board/{id}/lock
/board/{id}/lock/cancel
/board/{id}/config
/board/{id}/config/modify
/board/{id}/stats/update
/board/{id}/stats/update/replies
/board/stats/multi
/board/stats/all
/board/favorite/member/{member}
/board/star/member/{member}
/board/favorite/member/{member}/{page}/{number}
/board/all/{volume}
/board/all
/board/model
/board/{id}/check/{member}/operate
* */